﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MemberTypesManager.cs" company="CodePlex">
//   The MIT License
//   Copyright (c) 2013 Julien Blin
//   
//   Permission is hereby granted, free of charge, to any person obtaining a copy
//   of this software and associated documentation files (the "Software"), to deal
//   in the Software without restriction, including without limitation the rights
//   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//   copies of the Software, and to permit persons to whom the Software is
//   furnished to do so, subject to the following conditions:
//   
//   The above copyright notice and this permission notice shall be included in
//   all copies or substantial portions of the Software.
//   
//   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//   THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace UCodeFirst.MemberTypes
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    using umbraco.cms.businesslogic.member;

    using Umbraco.Core;
    using Umbraco.Core.Logging;

    /// <summary>
    /// Manager responsible for synchronizing member types.
    /// </summary>
    public class MemberTypesManager : UCodeFirstManagerBase
    {
#pragma warning disable 612,618

        /// <summary>
        /// The key to store and retrieve the list of synchronized member type aliases.
        /// </summary>
        private const string MemberTypesListKey = "MemberTypesList";

        /// <summary>
        /// List of synchronized member types.
        /// </summary>
        private IList<string> synchronizedMemberTypes;

        /// <summary>
        /// Returns <see cref="ManagerPriorities.MemberTypes"/>.
        /// </summary>
        public override int Priority
        {
            get
            {
                return ManagerPriorities.MemberTypes;
            }
        }

        /// <summary>
        /// All the managers <see cref="UCodeFirstManagerBase.Synchronize"/> methods will run after all the managers <see cref="UCodeFirstManagerBase.BeforeSynchronization"/> method have run,
        /// <b>only</b> if the synchronization happens.<br />
        /// They run in the order specified by the <see cref="UCodeFirstManagerBase.Priority"/>.
        /// </summary>
        /// <param name="httpApplication">
        ///     The <see cref="UmbracoApplicationBase"/>.
        /// </param>
        /// <param name="applicationContext">
        ///     The <see cref="ApplicationContext"/>.
        /// </param>
        public override void Synchronize(UmbracoApplicationBase httpApplication, ApplicationContext applicationContext)
        {
            this.synchronizedMemberTypes = new List<string>();
            var memberTypeBaseTypes = TypesScanner.GetClassesInBinThatAreAssignableFrom<MemberTypeBase>().OrderByClassInheritanceLevel().ToList();
            LogHelper.Debug<MemberTypesManager>(Resources.FoundNMemberTypeBase.InvariantFormat(memberTypeBaseTypes.Count));

            var allUmbracoMemberTypes = MemberType.GetAll;

            foreach (var memberTypeBaseType in memberTypeBaseTypes)
            {
                try
                {
                    LogHelper.Debug<MemberTypesManager>(Resources.Synchronizing.InvariantFormat(memberTypeBaseType.Name));

                    var memberTypeAttribute = memberTypeBaseType.GetAttr<MemberTypeAttribute>()
                                              ?? new MemberTypeAttribute();

                    var umbracoMemberType = allUmbracoMemberTypes.FirstOrDefault(x => x.Alias.InvariantEquals(memberTypeBaseType.Name))
                                            ?? MemberType.MakeNew(UCodeFirstConfig.GetUser(), memberTypeAttribute.GetName(memberTypeBaseType.Name));

                    var memberTypeUpdater = new ConditionalPropertyUpdater<MemberType>(umbracoMemberType);
                    memberTypeUpdater.Update(x => x.Alias, memberTypeBaseType.Name);
                    memberTypeUpdater.Update(x => x.Text, memberTypeAttribute.GetName(memberTypeBaseType.Name));
                    memberTypeUpdater.Update(x => x.IconUrl, memberTypeAttribute.Icon);
                    memberTypeUpdater.Update(x => x.Thumbnail, memberTypeAttribute.Thumbnail);
                    memberTypeUpdater.Update(x => x.Description, memberTypeAttribute.Description);

                    if (memberTypeUpdater.HasBeenChanged)
                    {
                        umbracoMemberType.Save();
                    }

                    this.SynchronizeTabs(umbracoMemberType, memberTypeBaseType);
                    this.SynchronizeProperties(umbracoMemberType, memberTypeBaseType);

                    this.synchronizedMemberTypes.Add(umbracoMemberType.Alias);

                    LogHelper.Debug<MemberTypesManager>(Resources.Synchronized.InvariantFormat(memberTypeBaseType.Name));
                }
                catch (Exception ex)
                {
                    throw new UCodeFirstException(Resources.ErrorWhileSynchronizing.InvariantFormat(memberTypeBaseType), ex);
                }
            }

            LogHelper.Info<MemberTypesManager>(Resources.SynchronizedNMemberTypeBase.InvariantFormat(memberTypeBaseTypes.Count));
        }

        /// <summary>
        /// The after synchronization.
        /// </summary>
        /// <param name="httpApplication">
        /// The http application.
        /// </param>
        /// <param name="applicationContext">
        /// The application context.
        /// </param>
        /// <param name="synchronizationHappened">
        /// The synchronization happened.
        /// </param>
        public override void AfterSynchronization(UmbracoApplicationBase httpApplication, ApplicationContext applicationContext, bool synchronizationHappened)
        {
            if (synchronizationHappened)
            {
                var previouslySynchronizedMemberTypes = new List<string>();
                var previouslySynchronizedMemberTypesValue = UCodeFirstData.GetValue<MemberTypesManager>(MemberTypesListKey);
                if (!string.IsNullOrEmpty(previouslySynchronizedMemberTypesValue))
                {
                    previouslySynchronizedMemberTypes = previouslySynchronizedMemberTypesValue.Split(new[] { "|" }, StringSplitOptions.RemoveEmptyEntries).ToList();
                }

                var memberTypesToDelete = MemberType.GetAll
                                                    .Where(x => previouslySynchronizedMemberTypes.InvariantContains(x.Alias)
                                                                && !this.synchronizedMemberTypes.InvariantContains(x.Alias))
                                                    .ToList();

                foreach (var memberTypeToDelete in memberTypesToDelete)
                {
                    try
                    {
                        LogHelper.Info<MemberTypesManager>(Resources.Deleting.InvariantFormat(memberTypeToDelete.Alias));
                        memberTypeToDelete.delete();
                    }
                    catch (Exception ex)
                    {
                        throw new UCodeFirstException(Resources.ErrorWhileDeleting.InvariantFormat(memberTypeToDelete.Alias), ex);
                    }
                }

                UCodeFirstData.SetValue<MemberTypesManager>(MemberTypesListKey, string.Join("|", this.synchronizedMemberTypes));
            }
        }

        /// <summary>
        /// Synchronizes the tabs.
        /// </summary>
        /// <param name="umbracoMemberType">
        /// The umbraco member type.
        /// </param>
        /// <param name="memberTypeBaseType">
        /// The member type base type.
        /// </param>
        private void SynchronizeTabs(MemberType umbracoMemberType, Type memberTypeBaseType)
        {
            var propertyGroups = memberTypeBaseType.GetProperties()
                                                   .Where(x => x.HasAttr<PropertyAttribute>())
                                                   .OrderBy(x => x.GetAttr<PropertyAttribute>().GetGroupOrder())
                                                   .GroupBy(x => x.GetAttr<PropertyAttribute>().GetGroupName())
                                                   .Select(x => new { Name = x.Key, Order = x.First().GetAttr<PropertyAttribute>().GetGroupOrder() })
                                                   .ToList();

            foreach (var propertyGroup in propertyGroups)
            {
                var tab = umbracoMemberType.getVirtualTabs.FirstOrDefault(t => t.Caption.InvariantEquals(propertyGroup.Name));
                var tabId = tab == null ? umbracoMemberType.AddVirtualTab(propertyGroup.Name) : tab.Id;

                if ((tab == null) || (tab.SortOrder != propertyGroup.Order))
                {
                    umbracoMemberType.SetTabSortOrder(tabId, propertyGroup.Order);
                }
            }

            var tabsToRemove = umbracoMemberType.getVirtualTabs.Where(x => !propertyGroups.Select(y => y.Name).Contains(x.Caption));
            foreach (var tabToRemove in tabsToRemove)
            {
                LogHelper.Debug<MemberTypesManager>(Resources.Deleting.InvariantFormat(tabToRemove.Caption));
                umbracoMemberType.DeleteVirtualTab(tabToRemove.Id);
            }
        }

        /// <summary>
        /// Synchronizes the properties.
        /// </summary>
        /// <param name="umbracoMemberType">
        /// The umbraco member type.
        /// </param>
        /// <param name="memberTypeBaseType">
        /// The member type base type.
        /// </param>
        private void SynchronizeProperties(MemberType umbracoMemberType, Type memberTypeBaseType)
        {
            var sortOrder = 0;
            var properties = memberTypeBaseType.GetProperties().Where(x => x.HasAttr<PropertyAttribute>()).ToList();
            foreach (var property in properties)
            {
                try
                {
                    var propertyAttr = property.GetAttr<PropertyAttribute>();
                    var dataTypeDefinition = umbraco.cms.businesslogic.datatype.DataTypeDefinition.GetDataTypeDefinition(propertyAttr.GetDataTypeDefinition().Id);
                    var umbracoProperty = umbracoMemberType.getPropertyType(property.Name)
                                          ?? umbracoMemberType.AddPropertyType(dataTypeDefinition, property.Name, propertyAttr.GetName(property.Name));

                    var propertyUpdater = new ConditionalPropertyUpdater<umbraco.cms.businesslogic.propertytype.PropertyType>(umbracoProperty);

                    propertyUpdater.Update(x => x.DataTypeDefinition, dataTypeDefinition);
                    propertyUpdater.Update(x => x.Name, propertyAttr.GetName(property.Name));
                    propertyUpdater.Update(x => x.Mandatory, propertyAttr.Mandatory);
                    propertyUpdater.Update(x => x.ValidationRegExp, propertyAttr.Validation);
                    propertyUpdater.Update(x => x.Description, propertyAttr.Description);
                    propertyUpdater.Update(x => x.SortOrder, sortOrder);

                    if (propertyUpdater.HasBeenChanged)
                    {
                        umbracoProperty.Save();
                    }

                    ++sortOrder;

                    var tab = umbracoMemberType.getVirtualTabs.FirstOrDefault(t => t.Caption.InvariantEquals(propertyAttr.GetGroupName()));
                    if (tab == null)
                    {
                        throw new UCodeFirstException(Resources.InternalError);
                    }

                    umbracoMemberType.SetTabOnPropertyType(umbracoProperty, tab.Id);
                }
                catch (Exception ex)
                {
                    throw new UCodeFirstException(Resources.ErrorWhileSynchronizing.InvariantFormat(property), ex);
                }
            }

            var propertiesToRemove = umbracoMemberType.PropertyTypes.Where(x => !properties.Select(y => y.Name).Contains(x.Alias));
            foreach (var propertyToRemove in propertiesToRemove)
            {
                LogHelper.Debug<MemberTypesManager>(Resources.Deleting.InvariantFormat(propertyToRemove.Name));
                propertyToRemove.delete();
            }
        }
#pragma warning restore 612,618
    }
}
